using System;
using System.Collections.Generic;
using System.Configuration;
using Navigant.Infrastructure.Cases;
using Navigant.Infrastructure.DomainBase;
using Navigant.Infrastructure.RepositoryFramework.Configuration;

namespace Navigant.Infrastructure.RepositoryFramework
{
    public static class RepositoryFactory
    {
        // Dictionary to enforce the singleton pattern
        private static Dictionary<string, object> repositories = new Dictionary<string, object>();

        /// <summary>
        /// Gets or creates an instance of the requested interface.  Once a 
        /// repository is created and initialized, it is cached, and all 
        /// future requests for the repository will come from the cache.
        /// </summary>
        /// <param name="currentCase">The data for the current <see cref="ICase"/> being accessed.</param>
        /// <typeparam name="TRepository">The interface of the repository 
        /// to create.</typeparam>
        /// <typeparam name="TEntity">The type of the Entity that the 
        /// repository is for.</typeparam>
        /// <param name="unitOfWork">The unit of work that the repository 
        /// will be participating in.</param>
        /// <returns>An instance of the interface requested.</returns>
        public static TRepository GetRepository<TRepository, 
            TEntity>(ICase currentCase, 
            IUnitOfWork unitOfWork, 
            bool runWithElevatedPrivileges) 
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            // Initialize the provider's default value
            TRepository repository = default(TRepository);

            string interfaceShortName = typeof(TRepository).Name;
            string entityTypeName = typeof(TEntity).Name;

            string repositoryCacheKey = string.Empty;
            if (currentCase != null)
            {
                repositoryCacheKey = string.Format("{0}:{1}:{2}", interfaceShortName, entityTypeName, currentCase.SiteAddress.AbsoluteUri);
            }
            else
            {
                repositoryCacheKey = string.Format("{0}:{1}", interfaceShortName, entityTypeName);
            }

            // See if the provider was already created and is in the cache 
            // or if there is a Unit of Work present...if there is a 
            // Unit of Work, the repository must be created each time,
            // it cannot come from the cache
            if (!RepositoryFactory.repositories.ContainsKey(repositoryCacheKey) || unitOfWork != null)
            {
                // Not there, so create it

                // Get the repositoryMappingsConfiguration config section
                RepositorySettings settings = (RepositorySettings)ConfigurationManager.GetSection(RepositoryMappingConstants.RepositoryMappingsConfigurationSectionName);

                // Get the type to be created
                Type repositoryType = null;

                // See if a valid interfaceShortName was passed in
                if (settings.RepositoryMappings.ContainsKey(interfaceShortName))
                {
                    repositoryType = Type.GetType(settings.RepositoryMappings[interfaceShortName].RepositoryFullTypeName);
                }
                else
                {
                    // It was not, but that's ok.  Now check to see if any of the 
                    // interface short names contain the name of the Entity.  If they
                    // do, then use the matching one to get the right Repository 
                    // Mapping Element
                    foreach (RepositoryMappingElement element in settings.RepositoryMappings)
                    {
                        if (element.InterfaceShortTypeName.Contains(entityTypeName))
                        {
                            repositoryType = Type.GetType(settings.RepositoryMappings[element.InterfaceShortTypeName].RepositoryFullTypeName);
                            break;
                        }
                    }
                }

                // Throw an exception if the right Repository 
                // Mapping Element could not be found and the resulting 
                // Repository Type could not be created
                if (repositoryType == null)
                {
                    throw new ArgumentNullException("Cannot create the Repository.  There was one or more invalid repositoryMapping configuration settings.");
                }

                // Build the constructor arguments
                List<object> constructorArgs = new List<object>();

                // See if an ICase needs to be injected to the repository's constructor
                if (currentCase != null)
                {
                    constructorArgs.Add(currentCase);
                }

                // See if an IUnitOfWork needs to be injected to the repository's constructor

                // Check if an IUnitOfWork was passed in and if the repository 
                // type to be created derives from RepositoryBase<T>
                if (unitOfWork != null && 
                    repositoryType.IsSubclassOf(typeof(RepositoryBase<TEntity>)))
                {
                    constructorArgs.Add(unitOfWork);
                }

                // Create the repository, and cast it to the interface specified
                repository = Activator.CreateInstance(repositoryType, constructorArgs.ToArray()) as TRepository;

                // Attempt to cache the newly created repository
                // Do NOT cache repositories that have a Unit of Work 
                // associated with them
                if (repository != null && unitOfWork == null)
                {
                    // Add the new provider instance to the cache
                    RepositoryFactory.repositories.Add(repositoryCacheKey, repository);
                }
            }
            else
            {
                // The provider was in the cache, so retrieve it
                repository = (TRepository)RepositoryFactory.repositories[repositoryCacheKey];
            }

            // Check configuration for runWithElevatedPrivileges
            if (!runWithElevatedPrivileges)
            {
                if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["runWithElevatedPrivileges"]))
                {
                    runWithElevatedPrivileges =
                        (ConfigurationManager.AppSettings["runWithElevatedPrivileges"].ToLower() == "true");
                }
            }

            if (repository is IRunWithElevatedPrivileges)
            {
                ((IRunWithElevatedPrivileges)repository).SetRunWithElevatedPrivileges(runWithElevatedPrivileges);
            }

            return repository;
        }

        public static TRepository GetRepository<TRepository, TEntity>(ICase currentCase)
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            return RepositoryFactory.GetRepository<TRepository, TEntity>(currentCase, null, false);
        }

        public static TRepository GetRepository<TRepository, TEntity>(IUnitOfWork unitOfWork)
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            return RepositoryFactory.GetRepository<TRepository, TEntity>(null, unitOfWork, false);
        }

        public static TRepository GetRepository<TRepository, TEntity>()
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            return RepositoryFactory.GetRepository<TRepository, TEntity>(null, null, false);
        }
    }
}
